[Design Pattern] Interpreter

[디자인 패턴][행위 패턴] 인터프리터

Posted by ChaelinJ on May 02, 2021

Interpreter

  • 문법 규칙을 클래스화 한 구조로, 일련의 규칙으로 정의된 문법적 언어를 해석하는 패턴
  • SQL 구문 분석, 기호 처리 엔진 등에서 사용

SQL과 같은 계층적 언어를 해석하기 위해 계층 구조를 표현할 수 있습니다.

여러 객체를 조합해 문법을 정의합니다. 각각의 객체는 특정한 문법을 처리할 수 있도록 구현됩니다.

이들이 모여 컴포지트 패턴과 같은 복합적인 트리 구조로 이루어지게 됩니다. 이는 하위 객체가 처리한 결과를 조합하여 새로운 결과를 만들어내게 해줍니다.

사용자가 원하는 다양한 명령을 쉽게 표현할 수 있게 구문 약속을 해야하며, 해석자에서는 이와 같이 약속된 구문이 입력 인자로 전달되었을 때 이를 해석할 수 있어야 합니다.

사용하기 좋은 경우

  • 정의할 언어의 문법이 간단!
  • 성능이 중요한 문제가 되지 않을 때

위와 같은 경우에 사용한다면 문법의 수정이나 새로운 문법을 추가하기 용이해지지만 문법이 복잡해진다면 문법을 정의하는 객체 구조가 복잡해져 관리가 어려워집니다.

간단한 문법에 한해 사용하길 권장됩니다

다이어그램

다이어그램으로 보니 Composite 패턴과 유사합니다.

컴퍼지트 패턴

  • Context: String 표현식이어야 하며, 인터프리터에 보내는 정보입니다.
  • AbstractExpression: 추상 구문 트리에 속한 모든 노드에 해당하는 클래스들이 공통으로 가져야할 연산을 정의하는 인터페이스입니다.
  • TerminalExpression: 문법에서 정의한 터미널 기호와 관련된 해석 방법을 구현합니다. 문법에 오른편(종착점)에 나타나는 모든 기호에 대해 클래스를 정의합니다.
  • NonterminalExpression: 논터미널 익스프레션에 대응하는 역할입니다.

예시와 이해

파일을 로드하고 삭제하는 명령어를 수행하는 프로그램을 인터프리터 패턴을 이용해 만들었습니다.

  • ActionExpression: NonTerminalExpression에 해당하는 부분으로, LOAD, DELETE 등의 명령어를 해석하는 부분
  • FileExpression: TerminalExpression에 해당하는 부분으로, file 타입과 file 이름이 입력되면 해당하는 FileExpression 객체의 fileList에 해당 file 이름을 저장합니다.
  • ActionExpression의 interprete(): ActionExpression에 저장된 FileExpression의 interprete()을 호출 합니다.

컴포지트 패턴을 이용해서 유연한 포함관계를 하려 하였으나 [명령어 - 파일리스트] 구조가 더 깔끔할 것 같아서 ActionExpression 내에 FileExpression만 포함되도록 하였습니다.

예시를 간단한 이미지로 보다면 다음과 같습니다.

Expression

interface AbstractExpression {
public void interprete();
}
class FileExpression implements AbstractExpression {
String type;
ArrayList<String> fileList = new ArrayList<String>();
public FileExpression(String type) {
// Type은 IMAGE, VIDEO, TEXT로 한정
if (type.equals("IMAGE")) {
this.type = "IMAGE";
} else if (type.equals("VIDEO")) {
this.type = "VIDEO";
} else if (type.equals("TEXT")) {
this.type = "TEXT";
} else {
System.out.println("type error");
}
}
public void addFile(String fileName) {
fileList.add(fileName);
}
@Override
public void interprete() {
String str = type + ": ";
for (String file : fileList) {
str += file + " ";
}
System.out.println(str);
}
}
class ActionExpression implements AbstractExpression {
private String actionName;
private HashMap<String, FileExpression> expressionList = new HashMap<String, FileExpression>();
public ActionExpression(String actionName) {
this.actionName = actionName;
}
public FileExpression getFile(String fileType) {
// File을 return
// 해당 type의 File이 없는 경우 생성 후 return
FileExpression file = expressionList.get(fileType);
if (file == null) {
expressionList.put(fileType, new FileExpression(fileType));
file = expressionList.get(fileType);
}
return file;
}
public HashMap<String, FileExpression> getExpressionList() {
return expressionList;
}
@Override
public void interprete() {
System.out.println("Action: " + actionName);
for (FileExpression expression : expressionList.values()) {
expression.interprete();
}
// 해석 완료 후 초기화
expressionList.clear();
}
}
view raw Expression.java hosted with ❤ by GitHub
  • ActionExpression은 HashMap으로 FileExpression을 저장하므로써, 한 파일 타입에 해당하는 객체를 중복되지 않게 가질 수 있습니다. 추가적으로 필요시에 생성되도록 할 수 있습니다.
  • ActionExpression은 Interprete 후 가지고 있던 FileExpression의 HashMap을 초기화 합니다. 이는 해당 Action 명령어에 대한 객체를 중복 생성하지 않고 재사용하기 때문입니다.

Interpreter

class Interpreter {
String context;
HashMap<String, ActionExpression> actionExpressions = new HashMap<String, ActionExpression>();
public Interpreter(String context) {
// 필요한 명령어 제한 및 생성
this.context = context;
actionExpressions.put("LOAD", new ActionExpression("LOAD"));
actionExpressions.put("DELETE", new ActionExpression("DELETE"));
}
public void setContext(String context) {
this.context = context;
}
public void run() {
// Context Interpret
StringTokenizer stk = new StringTokenizer(context);
ActionExpression actionExpression = null;
while (stk.hasMoreTokens()) {
String tmpContext = stk.nextToken();
AbstractExpression tmpExpression = actionExpressions.get(tmpContext);
if (tmpExpression != null) {
// Nonterminal인 경우: tmpContext는 LOAD, DELETE
// 한 Context 해석 중 Nonterminal 명령어가 두 번 이상 나올 경우, 이전 Nonterminal을 먼저 해석한 후 넘어간다.
if (actionExpression != null) {
actionExpression.interprete();
}
actionExpression = (ActionExpression) tmpExpression;
} else {
// Terminal인 경우 - tmpContext: 파일 타입
if (!stk.hasMoreTokens()) {
System.out.println("Input error: File Name");
}
String fileName = stk.nextToken();
FileExpression file = actionExpression.getFile(tmpContext);
file.addFile(fileName);
}
}
actionExpression.interprete();
}
}
  • ActionExpression 객체를 생성하고, Context를 입력 받아 분해 후 해석하는 동작을 하는 클래스 입니다.
  • ActionExpression 객체는 해당 객체 생성과 동시에 생성되며, Context가 변경되거나 하는 등의 동작이 있어도 수정/삭제되지 않고 재사용 됩니다.

Main

public class Main {
public static void main(String[] args) {
System.out.println("Context_01------------------------------");
String Context_01 = "LOAD IMAGE flower.jpg VIDEO happy.mp3";
Interpreter interpreter = new Interpreter(Context_01);
interpreter.run();
System.out.println("Context_02------------------------------");
String Context_02 = "DELETE IMAGE delete.jpg VIDEO blue.mp3";
interpreter.setContext(Context_02);
interpreter.run();
System.out.println("Context_03------------------------------");
String Context_03 = "LOAD IMAGE flower.jpg DELETE IMAGE delete.jpg VIDEO blue.mp3";
interpreter.setContext(Context_03);
interpreter.run();
}
}
view raw Main.java hosted with ❤ by GitHub

여러 형태로 Context를 생성했습니다.

세 번 째의 경우 Action 명령어가 두 번 포함되었습니다.

실행 결과는 다음과 같습니다.

출력

Context_01------------------------------
Action: LOAD
IMAGE: flower.jpg 
VIDEO: happy.mp3 
Context_02------------------------------
Action: DELETE
IMAGE: delete.jpg 
VIDEO: blue.mp3 
Context_03------------------------------
Action: LOAD
IMAGE: flower.jpg 
Action: DELETE
IMAGE: delete.jpg 
VIDEO: blue.mp3 

Action이 두 번 등장해도 오류 없이 잘 작동합니다.

간단한 문장 해석에도 굉장히 긴 코드가 필요하네요. 더 복잡해 질 경우엔 작성 시간과 수행 시간 모두 오래 걸릴 것 같습니다.

구현하기 너무 복잡한 경우, 컴파일러나 파서를 쓰는게 더 효율적일 수도 있다고 합니다.


참조

감사합니다.

Text by Chaelin. Photographs by Chaelin, Unsplash.